/**
 * Copyright 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule validateDOMNesting
 */

'use strict'

var _assign = require('object-assign')

var emptyFunction = require('fbjs/lib/emptyFunction')
var warning = require('fbjs/lib/warning')

var validateDOMNesting = emptyFunction

if (process.env.NODE_ENV !== 'production') {
  // This validation code was written based on the HTML5 parsing spec:
  // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
  //
  // Note: this does not catch all invalid nesting, nor does it try to (as it's
  // not clear what practical benefit doing so provides); instead, we warn only
  // for cases where the parser will give a parse tree differing from what React
  // intended. For example, <b><div></div></b> is invalid but we don't warn
  // because it still parses correctly; we do warn for other cases like nested
  // <p> tags where the beginning of the second element implicitly closes the
  // first, causing a confusing mess.

  // https://html.spec.whatwg.org/multipage/syntax.html#special
  var specialTags = [
    'address',
    'applet',
    'area',
    'article',
    'aside',
    'base',
    'basefont',
    'bgsound',
    'blockquote',
    'body',
    'br',
    'button',
    'caption',
    'center',
    'col',
    'colgroup',
    'dd',
    'details',
    'dir',
    'div',
    'dl',
    'dt',
    'embed',
    'fieldset',
    'figcaption',
    'figure',
    'footer',
    'form',
    'frame',
    'frameset',
    'h1',
    'h2',
    'h3',
    'h4',
    'h5',
    'h6',
    'head',
    'header',
    'hgroup',
    'hr',
    'html',
    'iframe',
    'img',
    'input',
    'isindex',
    'li',
    'link',
    'listing',
    'main',
    'marquee',
    'menu',
    'menuitem',
    'meta',
    'nav',
    'noembed',
    'noframes',
    'noscript',
    'object',
    'ol',
    'p',
    'param',
    'plaintext',
    'pre',
    'script',
    'section',
    'select',
    'source',
    'style',
    'summary',
    'table',
    'tbody',
    'td',
    'template',
    'textarea',
    'tfoot',
    'th',
    'thead',
    'title',
    'tr',
    'track',
    'ul',
    'wbr',
    'xmp'
  ]

  // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
  var inScopeTags = [
    'applet',
    'caption',
    'html',
    'table',
    'td',
    'th',
    'marquee',
    'object',
    'template',

    // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
    // TODO: Distinguish by namespace here -- for <title>, including it here
    // errs on the side of fewer warnings
    'foreignObject',
    'desc',
    'title'
  ]

  // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
  var buttonScopeTags = inScopeTags.concat(['button'])

  // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
  var impliedEndTags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt']

  var emptyAncestorInfo = {
    current: null,

    formTag: null,
    aTagInScope: null,
    buttonTagInScope: null,
    nobrTagInScope: null,
    pTagInButtonScope: null,

    listItemTagAutoclosing: null,
    dlItemTagAutoclosing: null
  }

  var updatedAncestorInfo = function (oldInfo, tag, instance) {
    var ancestorInfo = _assign({}, oldInfo || emptyAncestorInfo)
    var info = { tag: tag, instance: instance }

    if (inScopeTags.indexOf(tag) !== -1) {
      ancestorInfo.aTagInScope = null
      ancestorInfo.buttonTagInScope = null
      ancestorInfo.nobrTagInScope = null
    }
    if (buttonScopeTags.indexOf(tag) !== -1) {
      ancestorInfo.pTagInButtonScope = null
    }

    // See rules for 'li', 'dd', 'dt' start tags in
    // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
    if (
      specialTags.indexOf(tag) !== -1 &&
      tag !== 'address' &&
      tag !== 'div' &&
      tag !== 'p'
    ) {
      ancestorInfo.listItemTagAutoclosing = null
      ancestorInfo.dlItemTagAutoclosing = null
    }

    ancestorInfo.current = info

    if (tag === 'form') {
      ancestorInfo.formTag = info
    }
    if (tag === 'a') {
      ancestorInfo.aTagInScope = info
    }
    if (tag === 'button') {
      ancestorInfo.buttonTagInScope = info
    }
    if (tag === 'nobr') {
      ancestorInfo.nobrTagInScope = info
    }
    if (tag === 'p') {
      ancestorInfo.pTagInButtonScope = info
    }
    if (tag === 'li') {
      ancestorInfo.listItemTagAutoclosing = info
    }
    if (tag === 'dd' || tag === 'dt') {
      ancestorInfo.dlItemTagAutoclosing = info
    }

    return ancestorInfo
  }

  /**
   * Returns whether
   */
  var isTagValidWithParent = function (tag, parentTag) {
    // First, let's check if we're in an unusual parsing mode...
    switch (parentTag) {
      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
      case 'select':
        return tag === 'option' || tag === 'optgroup' || tag === '#text'
      case 'optgroup':
        return tag === 'option' || tag === '#text'
      // Strictly speaking, seeing an <option> doesn't mean we're in a <select>
      // but
      case 'option':
        return tag === '#text'

      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
      // No special behavior since these rules fall back to "in body" mode for
      // all except special table nodes which cause bad parsing behavior anyway.

      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
      case 'tr':
        return (
          tag === 'th' ||
          tag === 'td' ||
          tag === 'style' ||
          tag === 'script' ||
          tag === 'template'
        )

      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
      case 'tbody':
      case 'thead':
      case 'tfoot':
        return (
          tag === 'tr' ||
          tag === 'style' ||
          tag === 'script' ||
          tag === 'template'
        )

      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
      case 'colgroup':
        return tag === 'col' || tag === 'template'

      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
      case 'table':
        return (
          tag === 'caption' ||
          tag === 'colgroup' ||
          tag === 'tbody' ||
          tag === 'tfoot' ||
          tag === 'thead' ||
          tag === 'style' ||
          tag === 'script' ||
          tag === 'template'
        )

      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
      case 'head':
        return (
          tag === 'base' ||
          tag === 'basefont' ||
          tag === 'bgsound' ||
          tag === 'link' ||
          tag === 'meta' ||
          tag === 'title' ||
          tag === 'noscript' ||
          tag === 'noframes' ||
          tag === 'style' ||
          tag === 'script' ||
          tag === 'template'
        )

      // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
      case 'html':
        return tag === 'head' || tag === 'body'
      case '#document':
        return tag === 'html'
    }

    // Probably in the "in body" parsing mode, so we outlaw only tag combos
    // where the parsing rules cause implicit opens or closes to be added.
    // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
    switch (tag) {
      case 'h1':
      case 'h2':
      case 'h3':
      case 'h4':
      case 'h5':
      case 'h6':
        return (
          parentTag !== 'h1' &&
          parentTag !== 'h2' &&
          parentTag !== 'h3' &&
          parentTag !== 'h4' &&
          parentTag !== 'h5' &&
          parentTag !== 'h6'
        )

      case 'rp':
      case 'rt':
        return impliedEndTags.indexOf(parentTag) === -1

      case 'caption':
      case 'col':
      case 'colgroup':
      case 'frame':
      case 'head':
      case 'html':
      case 'tbody':
      case 'td':
      case 'tfoot':
      case 'th':
      case 'thead':
      case 'tr':
        // These tags are only valid with a few parents that have special child
        // parsing rules -- if we're down here, then none of those matched and
        // so we allow it only if we don't know what the parent is, as all other
        // cases are invalid.
        return parentTag == null
    }

    return true
  }

  /**
   * Returns whether
   */
  var findInvalidAncestorForTag = function (tag, ancestorInfo) {
    switch (tag) {
      case 'address':
      case 'article':
      case 'aside':
      case 'blockquote':
      case 'center':
      case 'details':
      case 'dialog':
      case 'dir':
      case 'div':
      case 'dl':
      case 'fieldset':
      case 'figcaption':
      case 'figure':
      case 'footer':
      case 'header':
      case 'hgroup':
      case 'main':
      case 'menu':
      case 'nav':
      case 'ol':
      case 'p':
      case 'section':
      case 'summary':
      case 'ul':

      case 'pre':
      case 'listing':

      case 'table':

      case 'hr':

      case 'xmp':

      case 'h1':
      case 'h2':
      case 'h3':
      case 'h4':
      case 'h5':
      case 'h6':
        return ancestorInfo.pTagInButtonScope

      case 'form':
        return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope

      case 'li':
        return ancestorInfo.listItemTagAutoclosing

      case 'dd':
      case 'dt':
        return ancestorInfo.dlItemTagAutoclosing

      case 'button':
        return ancestorInfo.buttonTagInScope

      case 'a':
        // Spec says something about storing a list of markers, but it sounds
        // equivalent to this check.
        return ancestorInfo.aTagInScope

      case 'nobr':
        return ancestorInfo.nobrTagInScope
    }

    return null
  }

  /**
   * Given a ReactCompositeComponent instance, return a list of its recursive
   * owners, starting at the root and ending with the instance itself.
   */
  var findOwnerStack = function (instance) {
    if (!instance) {
      return []
    }

    var stack = []
    do {
      stack.push(instance)
    } while ((instance = instance._currentElement._owner))
    stack.reverse()
    return stack
  }

  var didWarn = {}

  validateDOMNesting = function (childTag, childInstance, ancestorInfo) {
    ancestorInfo = ancestorInfo || emptyAncestorInfo
    var parentInfo = ancestorInfo.current
    var parentTag = parentInfo && parentInfo.tag

    var invalidParent = isTagValidWithParent(childTag, parentTag)
      ? null
      : parentInfo
    var invalidAncestor = invalidParent
      ? null
      : findInvalidAncestorForTag(childTag, ancestorInfo)
    var problematic = invalidParent || invalidAncestor

    if (problematic) {
      var ancestorTag = problematic.tag
      var ancestorInstance = problematic.instance

      var childOwner = childInstance && childInstance._currentElement._owner
      var ancestorOwner =
        ancestorInstance && ancestorInstance._currentElement._owner

      var childOwners = findOwnerStack(childOwner)
      var ancestorOwners = findOwnerStack(ancestorOwner)

      var minStackLen = Math.min(childOwners.length, ancestorOwners.length)
      var i

      var deepestCommon = -1
      for (i = 0; i < minStackLen; i++) {
        if (childOwners[i] === ancestorOwners[i]) {
          deepestCommon = i
        } else {
          break
        }
      }

      var UNKNOWN = '(unknown)'
      var childOwnerNames = childOwners
        .slice(deepestCommon + 1)
        .map(function (inst) {
          return inst.getName() || UNKNOWN
        })
      var ancestorOwnerNames = ancestorOwners
        .slice(deepestCommon + 1)
        .map(function (inst) {
          return inst.getName() || UNKNOWN
        })
      var ownerInfo = []
        .concat(
          // If the parent and child instances have a common owner ancestor, start
          // with that -- otherwise we just start with the parent's owners.
          deepestCommon !== -1
            ? childOwners[deepestCommon].getName() || UNKNOWN
            : [],
          ancestorOwnerNames,
          ancestorTag,
          // If we're warning about an invalid (non-parent) ancestry, add '...'
          invalidAncestor ? ['...'] : [],
          childOwnerNames,
          childTag
        )
        .join(' > ')

      var warnKey =
        !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + ownerInfo
      if (didWarn[warnKey]) {
        return
      }
      didWarn[warnKey] = true

      var tagDisplayName = childTag
      if (childTag !== '#text') {
        tagDisplayName = '<' + childTag + '>'
      }

      if (invalidParent) {
        var info = ''
        if (ancestorTag === 'table' && childTag === 'tr') {
          info +=
            ' Add a <tbody> to your code to match the DOM tree generated by ' +
            'the browser.'
        }
        process.env.NODE_ENV !== 'production'
          ? warning(
              false,
              'validateDOMNesting(...): %s cannot appear as a child of <%s>. ' +
                'See %s.%s',
              tagDisplayName,
              ancestorTag,
              ownerInfo,
              info
            )
          : void 0
      } else {
        process.env.NODE_ENV !== 'production'
          ? warning(
              false,
              'validateDOMNesting(...): %s cannot appear as a descendant of ' +
                '<%s>. See %s.',
              tagDisplayName,
              ancestorTag,
              ownerInfo
            )
          : void 0
      }
    }
  }

  validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo

  // For testing
  validateDOMNesting.isTagValidInContext = function (tag, ancestorInfo) {
    ancestorInfo = ancestorInfo || emptyAncestorInfo
    var parentInfo = ancestorInfo.current
    var parentTag = parentInfo && parentInfo.tag
    return (
      isTagValidWithParent(tag, parentTag) &&
      !findInvalidAncestorForTag(tag, ancestorInfo)
    )
  }
}

module.exports = validateDOMNesting
